package com.nfmn;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.regex.Pattern;

import static io.netty.handler.codec.http.HttpHeaders.Names;
import static io.netty.handler.codec.http.HttpHeaders.Values;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

public class Server {
    static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

    public static void main(String[] args) throws Exception {
        EventLoopGroup parent = new NioEventLoopGroup(2);
        EventLoopGroup child = new NioEventLoopGroup(4);
        int port = 8000;
        if (args != null && args.length > 0) {
            port = Integer.parseInt(args[0]);
        }
        System.out.println("启动http服务，端口:" + port);
        try {
            ServerBootstrap starter = new ServerBootstrap();
            starter.group(parent, child)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new Initializer());

            starter.bind(port).sync().channel().closeFuture().sync();
        } finally {
            parent.shutdownGracefully();
            child.shutdownGracefully();
        }
    }

    static class Initializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast("decoder", new HttpRequestDecoder());
            pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
            pipeline.addLast("encoder", new HttpResponseEncoder());
            pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());

            //pipeline.addLast(new HttpServerCodec());

            //pipeline.addLast(new HttpContentCompressor());
            pipeline.addLast("handler", new Handler());
        }
    }

    static class Handler extends SimpleChannelInboundHandler<FullHttpRequest> {

        private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
            FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
            response.headers().set(Names.LOCATION, newUri);
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

        private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
            ByteBuf body = Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8);
            FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, body);
            response.headers().set(Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

        private static void setContentTypeHeader(HttpHeaders headers, File file) {
            headers.set(Names.CACHE_CONTROL, Values.NO_CACHE);
            headers.set(Names.PRAGMA, Values.NO_CACHE);
            headers.set(Names.EXPIRES, -1);
            headers.set(Names.CONNECTION, Values.CLOSE);
            if (file == null) {
                headers.set(Names.CONTENT_TYPE, "text/html; charset=UTF-8");
            } else {
                headers.set(Names.CONTENT_LENGTH, file.length());
                MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
                String type = mimeTypesMap.getContentType(file);
                headers.set(Names.CONTENT_TYPE, type);
                if ("application/octet-stream".equals(type)) {
                    try {
                        headers.set("Content-Disposition", "attachment; filename=" + URLEncoder.encode(file.getName(), "utf-8"));
                    } catch (UnsupportedEncodingException e) {
                    }
                }
                System.out.println(type);
            }
        }

        private static String sanitizeUri(String uri) {
            String rootPath = System.getProperty("user.dir");
            if (null == uri || uri.length() == 0 || "/".equals(uri)) {
                return rootPath;
            }
            try {
                uri = URLDecoder.decode(uri, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                try {
                    uri = URLDecoder.decode(uri, "ISO-8859-1");
                } catch (UnsupportedEncodingException e1) {
                    throw new Error();
                }
            }

            uri = uri.replace('/', File.separatorChar);

            if (uri.contains(File.separator + '.') ||
                    uri.contains('.' + File.separator) ||
                    uri.startsWith(".") || uri.endsWith(".") ||
                    INSECURE_URI.matcher(uri).matches()) {
                return null;
            }

            if (endsWith(rootPath, File.separatorChar) && startsWith(uri, File.separatorChar)) {
                rootPath = rootPath.substring(0, rootPath.length() - 1);
            }
            return rootPath + uri;
        }

        private static void sendListing(ChannelHandlerContext ctx, File dir) {
            StringBuilder buf = new StringBuilder();
            String dirPath = dir.getPath();

            buf.append("<!DOCTYPE html>\r\n");
            buf.append("<html><head><title>");
            buf.append("Listing of: ");
            buf.append(dirPath);
            buf.append("</title></head><body>\r\n");

            buf.append("<h2>Directory listing for ");
            buf.append(dirPath);
            buf.append("</h2>\r\n");

            buf.append("<hr>");
            buf.append("<ul>");
            buf.append("<li><a href=\"../\">..</a></li>\r\n");

            for (File f : dir.listFiles()) {
                if (!f.canRead()) {
                    continue;
                }
                if (f.isHidden()) {
                    continue;
                }

                String name = f.getName();
                buf.append("<li><a href=\"");
                try {
                    buf.append(URLEncoder.encode(name, "utf-8"));
                } catch (UnsupportedEncodingException e) {
                }
                buf.append("\">");
                buf.append(name);
                buf.append("</a></li>\r\n");
            }

            buf.append("</ul></body></html>\r\n");
            buf.append("<hr>");

            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK,
                    Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8));
            setContentTypeHeader(response.headers(), null);
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
            if (!request.getDecoderResult().isSuccess()) {
                sendError(ctx, BAD_REQUEST);
                return;
            }

            if (request.getMethod() != GET) {
                sendError(ctx, METHOD_NOT_ALLOWED);
                return;
            }

            final String uri = request.getUri();
            final String path = sanitizeUri(uri);
            System.out.println(uri);
            System.out.println(path);
            if (path == null) {
                sendError(ctx, FORBIDDEN);
                return;
            }

            File file = new File(path);
            if (!file.exists()) {
                sendError(ctx, NOT_FOUND);
                return;
            }

            if (file.isDirectory()) {
                if (uri.endsWith("/")) {
                    sendListing(ctx, file);
                } else {
                    sendRedirect(ctx, uri + '/');
                }
                return;
            }

            if (!file.isFile()) {
                sendError(ctx, FORBIDDEN);
                return;
            }

            RandomAccessFile raf;
            try {
                raf = new RandomAccessFile(file, "r");
            } catch (FileNotFoundException fnfe) {
                fnfe.printStackTrace();
                sendError(ctx, NOT_FOUND);
                return;
            }

            DefaultHttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
            setContentTypeHeader(response.headers(), file);
            ctx.write(response);
            ChannelFuture writeFuture;
            if (raf.length() > 8192) {
                writeFuture = ctx.writeAndFlush(new ChunkedFile(raf, 0, raf.length(), 8192));
            } else {
                byte[] data = new byte[(int) raf.length()];
                raf.readFully(data);
                writeFuture = ctx.writeAndFlush(ctx.alloc().buffer(data.length).writeBytes(data));
            }
            writeFuture.addListener(ChannelFutureListener.CLOSE);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            if (ctx.channel().isActive()) {
                sendError(ctx, INTERNAL_SERVER_ERROR);
            }
        }
    }

    private static boolean endsWith(String str, char c) {
        if (str == null || str.length() == 0) {
            return false;
        }
        return str.charAt(str.length() - 1) == c;
    }

    private static boolean startsWith(String str, char c) {
        if (str == null || str.length() == 0) {
            return false;
        }
        return str.charAt(0) == c;
    }
}